import asyncio
import random
from struct import unpack
import socket
import aiohttp
import logging
from urllib.parse import urlencode

from torrent import Torrent
from bencoding import Decoder

class TrackerRespone:
    def __init__(self, response) -> None:
        self._response = response
    
    @property
    def failure(self) -> str:
        if b'failure reason' in self._respone:
            return self._response[b'failure reason'].decode('utf-8')
        return None
    
    @property
    def interval(self) -> int:
        return self._response.get(b'interval', 10 * 60)

    @property
    def complete(self) -> int:
        return self._response.get(b'complete', 0)

    @property
    def incomplete(self) -> int:
        return self._response.get(b'incomplete', 0)

    @property
    def peers(self) -> list:
        # return [('192.168.25.135', '22223')] # local net hack

        peers = self._response[b'peers']
        if type(peers) == list:
            logging.debug('List model peers returned from tracker')
            raise NotImplementedError()
        elif type(peers) == bytes:
            logging.debug('Bytes model peers returned from tracker')
            # each 6 bytes = 4 bytes of ip +  2 bytes of port
            assert len(peers) % 6 == 0
            peers = [peers[i : i+6] for i in range(0, len(peers), 6)]

            peers = [(socket.inet_ntoa(peer[:4]), _decode_port(peer[4:])) for peer in peers]
            return peers

    def __str__(self):
        return "incomplete: {incomplete}\n" \
               "complete: {complete}\n" \
               "interval: {interval}\n" \
               "peers count: {peers_count}\n" \
               "peers: {peers}\n".format(
                   incomplete=self.incomplete,
                   complete=self.complete,
                   interval=self.interval,
                   peers_count=len(self.peers),
                   peers=", ".join(["{}:{}".format(x, p) for (x, p) in self.peers]))

    

class Tracker:
    def __init__(self, torrent: Torrent) -> None:
        self.torrent = torrent
        self.peer_id = _calculate_peer_id()
        self.http_client = aiohttp.ClientSession()

        logging.info('Our peer_id is: {}'.format(self.peer_id))

    async def connect(self, 
                      first: bool=None,
                      uploaded: int=0,
                      downloaded: int=0):
        params = {
            'info_hash' : self.torrent.info_hash,
            'peer_id'   : self.peer_id,
            'port'      : 6889,
            'uploaded'  : uploaded,
            'downloaded': downloaded,
            'left'      : self.torrent.total_size - downloaded,
            'compact'   : 1
        }
        if first:
            params['event'] = 'started'

        url = self.torrent.announce + '?' + urlencode(params)
        logging.info('Connecting to tracker: ' + url)

        async with self.http_client.get(url) as response:
            if not response.status == 200:
                raise ConnectionError('Respone status: {}'.format(response.status))
            data = await response.read()
            return TrackerRespone(Decoder(data).decode())
    
    def close(self):
        self.http_client.close()


def _calculate_peer_id():
    """
    Calculate and return a unique Peer ID.

    The `peer id` is a 20 byte long identifier. This implementation use the
    Azureus style `-PC1000-<random-characters>`.

    Read more:
        https://wiki.theory.org/BitTorrentSpecification#peer_id
    """
    return ('-mB0001-' + ''.join(
        [str(random.randint(0, 9)) for _ in range(12)])).encode('utf-8')

def _decode_port(port: bytes) -> int:
    # big endian -> ushort
    return unpack('>H', port)[0]